﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data.Entity.Core.Metadata.Edm;
using System.Globalization;
using System.Linq;

namespace Sidvall.CodeGenerator
{
    public class TypeMapper
    {
        public const string DbGeographyTypeName = "System.Data.Entity.Spatial.DbGeography";
        public const string DbGeometryTypeName = "System.Data.Entity.Spatial.DbGeometry";

        #region PluralEndings

        private static Collection<string> _PluralEndings;
        public static Collection<string> PluralEndings
        {
            get
            {
                Collection<string> items;

                if (_PluralEndings == null)
                {
                    items = new Collection<string>();
                    items.Add("claims");
                    items.Add("data");
                    items.Add("extended");
                    items.Add("info");
                    items.Add("items");
                    items.Add("logins");
                    items.Add("news");
                    items.Add("origins");
                    items.Add("redirecturis");
                    items.Add("resources");
                    items.Add("restrictions");
                    items.Add("roles");
                    items.Add("secrets");
                    items.Add("users");
                    items.Add("scopes");
                    items.Add("types");
                    _PluralEndings = items;
                }
                return _PluralEndings;
            }
        }

        #endregion
        #region EsEndings

        private static Collection<string> _EsEndings;
        public static Collection<string> EsEndings
        {
            get
            {
                Collection<string> items;

                if (_EsEndings == null)
                {
                    items = new Collection<string>();
                    items.Add("s");
                    items.Add("x");
                    items.Add("ch");
                    _EsEndings = items;
                }
                return _EsEndings;
            }
        }

        #endregion

        #region ConvertCaseString

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
        public static string ConvertCaseString(string value, CaseType caseType)
        {
            if (string.IsNullOrWhiteSpace(value))
                return value;
            switch (caseType)
            {
                case CaseType.CamelCase:
                    return value[0].ToString().ToLower(System.Globalization.CultureInfo.InvariantCulture) + value.Substring(1);
                default:
                    return value[0].ToString().ToUpper(System.Globalization.CultureInfo.InvariantCulture) + value.Substring(1);
            }
        }

        #endregion
        #region IsValueType

        public static bool IsValueType(PropertyType type)
        {
            switch (type)
            {
                case PropertyType.Boolean:
                case PropertyType.Char:
                case PropertyType.DateTime:
                case PropertyType.DateTimeOffset:
                case PropertyType.Decimal:
                case PropertyType.Double:
                case PropertyType.Byte:
                case PropertyType.Int16:
                case PropertyType.Int32:
                case PropertyType.Int64:
                case PropertyType.SByte:
                case PropertyType.Single:
                case PropertyType.TimeSpan:
                case PropertyType.UInt16:
                case PropertyType.UInt32:
                case PropertyType.UInt64:
                    return true;
                default:
                    return false;
            }
        }

        #endregion
        #region GetPropertyType

        public static PropertyType GetPropertyType(string typeName)
        {
            if (string.IsNullOrWhiteSpace(typeName))
                return PropertyType.Unknown;
            switch (typeName.ToLowerInvariant())
            {
                case "system.boolean":
                case "boolean":
                case "bool":
                    return CodeGenerator.PropertyType.Boolean;
                case "system.char":
                case "char":
                    return CodeGenerator.PropertyType.Char;
                case "system.datetime":
                case "datetime":
                    return CodeGenerator.PropertyType.DateTime;
                case "system.datetimeoffset":
                case "datetimeoffset":
                    return CodeGenerator.PropertyType.DateTimeOffset;
                case "system.decimal":
                case "decimal":
                    return CodeGenerator.PropertyType.Decimal;
                case "system.double":
                case "double":
                    return CodeGenerator.PropertyType.Double;
                case "system.byte":
                case "byte":
                    return CodeGenerator.PropertyType.Byte;
                case "system.int16":
                case "int16":
                case "short":
                    return CodeGenerator.PropertyType.Int16;
                case "system.int32":
                case "int32":
                case "int":
                    return CodeGenerator.PropertyType.Int32;
                case "system.int64":
                case "int64":
                case "long":
                    return CodeGenerator.PropertyType.Int64;
                case "system.object":
                case "object":
                    return CodeGenerator.PropertyType.Object;
                case "system.sbyte":
                case "sbyte":
                    return CodeGenerator.PropertyType.SByte;
                case "system.single":
                case "single":
                case "float":
                    return CodeGenerator.PropertyType.Single;
                case "system.string":
                case "string":
                    return CodeGenerator.PropertyType.String;
                case "system.timespan":
                case "timespan":
                    return CodeGenerator.PropertyType.TimeSpan;
                case "system.uint16":
                case "uint16":
                case "ushort":
                    return CodeGenerator.PropertyType.UInt16;
                case "system.uint32":
                case "uint32":
                case "uint":
                    return CodeGenerator.PropertyType.UInt32;
                case "system.uint64":
                case "uint64":
                case "ulong":
                    return CodeGenerator.PropertyType.UInt64;
                case "system.xml":
                case "xml":
                    return CodeGenerator.PropertyType.Xml;
                case "system.guid":
                case "guid":
                    return CodeGenerator.PropertyType.Guid;
                case "system.byte[]":
                case "byte[]":
                    return CodeGenerator.PropertyType.Binary;
                case "sidvall.data.geography":
                case "system.data.entity.spatial.dbgeography":
                    return CodeGenerator.PropertyType.Geography;
                case "sidvall.data.geometry":
                case "system.data.entity.spatial.dbgeometry":
                    return CodeGenerator.PropertyType.Geometry;
                default:
                    return CodeGenerator.PropertyType.Unknown;
            }
        }

        #endregion
        #region GetTypeName

        public static string GetTypeName(PropertyType type)
        {
            switch (type)
            {
                case PropertyType.Boolean:
                    return "System.Boolean";
                case PropertyType.Char:
                    return "System.Char";
                case PropertyType.DateTime:
                    return "System.DateTime";
                case PropertyType.DateTimeOffset:
                    return "System.DateTimeOffset";
                case PropertyType.Decimal:
                    return "System.Decimal";
                case PropertyType.Double:
                    return "System.Double";
                case PropertyType.Byte:
                    return "System.Byte";
                case PropertyType.Int16:
                    return "System.Int16";
                case PropertyType.Int32:
                    return "System.Int32";
                case PropertyType.Int64:
                    return "System.Int64";
                case PropertyType.Object:
                    return "System.Object";
                case PropertyType.SByte:
                    return "System.Sbyte";
                case PropertyType.Single:
                    return "System.Single";
                case PropertyType.String:
                    return "System.String";
                case PropertyType.TimeSpan:
                    return "System.TimeSpan";
                case PropertyType.UInt16:
                    return "System.Uint16";
                case PropertyType.UInt32:
                    return "System.Uint32";
                case PropertyType.UInt64:
                    return "System.Uint64";
                case PropertyType.Xml:
                    return "System.Xml";
                case PropertyType.Guid:
                    return "System.Guid";
                case PropertyType.Binary:
                    return "System.Byte[]";
                default:
                    return null;
            }
        }
        public static string GetTypeName(string typeName, bool isNullable)
        {
            int index;

            if (string.IsNullOrWhiteSpace(typeName))
                return typeName;

            if (isNullable)
            {
                if (typeName.EndsWith("?"))
                    return typeName;
                if (typeName.StartsWith("system.nullable<", StringComparison.OrdinalIgnoreCase))
                    return typeName;
                if (typeName.StartsWith("nullable<", StringComparison.OrdinalIgnoreCase))
                    return typeName;
                return "System.Nullable<" + typeName + ">";
            }
            else
            {
                typeName = typeName.TrimEnd('?');
                index = typeName.IndexOf("nullable<", System.StringComparison.OrdinalIgnoreCase);
                if (index >= 0)
                    typeName = typeName.Substring(index + 9, typeName.Length - index - 10);
                return typeName;
            }
        }

        #endregion
        #region GetTypeNameInWeb

        public static string GetTypeNameInWeb(PropertyType type)
        {
            switch (type)
            {
                case PropertyType.Boolean:
                    return "boolean";
                case PropertyType.Decimal:
                case PropertyType.Double:
                case PropertyType.Byte:
                case PropertyType.Int16:
                case PropertyType.Int32:
                case PropertyType.Int64:
                case PropertyType.SByte:
                case PropertyType.Single:
                case PropertyType.UInt16:
                case PropertyType.UInt32:
                case PropertyType.UInt64:
                    return "number";
                case PropertyType.Char:
                case PropertyType.DateTime:
                case PropertyType.String:
                case PropertyType.Xml:
                case PropertyType.Guid:
                    return "string";
                default:
                    return "any";
            }
        }

        #endregion
        #region GetPluralName

        public static string GetPluralName(string name)
        {
            if (string.IsNullOrEmpty(name) == true)
                return string.Empty;
            name = name.Trim('_');
            foreach (var pluralEnding in PluralEndings)
            {
                if (name.EndsWith(pluralEnding, StringComparison.OrdinalIgnoreCase) == true)
                {
                    return name;
                }
            }
            foreach (var esEnding in EsEndings)
            {
                if (name.EndsWith(esEnding, StringComparison.OrdinalIgnoreCase) == true)
                {
                    return name + "es";
                }
            }
            if (name.EndsWith("y", StringComparison.OrdinalIgnoreCase) == true)
            {
                if ((name.Length > 2) && (Vowals.IndexOf(name.Substring(name.Length - 2, 1), StringComparison.OrdinalIgnoreCase) >= 0))
                    name += "s";
                else
                    name = name.Substring(0, name.Length - 1) + "ies";
            }
            else
                name += "s";
            return name;
        }

        #endregion
        #region GetSingularName

        public static string GetSingularName(string name)
        {
            if (string.IsNullOrEmpty(name) == true)
                return string.Empty;
            name = name.Trim('_');
            foreach (var pluralEnding in PluralEndings)
            {
                if ((name.EndsWith(pluralEnding, StringComparison.OrdinalIgnoreCase) == true) &&
                    (name.EndsWith("s", StringComparison.OrdinalIgnoreCase) == true))
                {
                    return name.Substring(0, name.Length - 1);
                }
            }
            return name;
        }

        #endregion
        #region GetDefaultValue

        public static string GetDefaultValue(PropertyType propertyType, bool isNullable)
        {
            switch (propertyType)
            {
                case PropertyType.Boolean:
                    return "false";
                case PropertyType.Char:
                    return "char.MinValue";
                case PropertyType.DateTime:
                    return "System.DateTime.UtcNow";
                case PropertyType.DateTimeOffset:
                    return "System.DateTimeOffset.UtcNow";
                case PropertyType.Decimal:
                    return "0M";
                case PropertyType.Double:
                    return "0D";
                case PropertyType.Byte:
                    return "(byte)0";
                case PropertyType.Int16:
                    return "(short)0";
                case PropertyType.Int32:
                    return "0";
                case PropertyType.Int64:
                    return "0L";
                case PropertyType.Object:
                    return "null";
                case PropertyType.SByte:
                    return "(sbyte)0";
                case PropertyType.Single:
                    return "0F";
                case PropertyType.String:
                    if (isNullable)
                        return "null";
                    else
                        return "string.Empty";
                case PropertyType.TimeSpan:
                    if (isNullable)
                        return "null";
                    else
                        return "System.TimeSpan.Zero";
                case PropertyType.UInt16:
                    return "(ushort)0";
                case PropertyType.UInt32:
                    return "0U";
                case PropertyType.UInt64:
                    return "0UL";
                case PropertyType.Xml:
                    return "null";
                case PropertyType.Guid:
                    return "System.Guid.NewGuid()";
                case PropertyType.Binary:
                    return "null";
                default:
                    return "null";
            }
        }

        #endregion
        #region GetExampleValue

        public static string GetExampleValue(PropertyType propertyType)
        {
            switch (propertyType)
            {
                case PropertyType.Boolean:
                    return "false";
                case PropertyType.Char:
                    return "a";
                case PropertyType.DateTime:
                    return "2010-09-05T21:23:12";
                case PropertyType.DateTimeOffset:
                    return "2010-09-05T21:23:12+02:00";
                case PropertyType.Decimal:
                    return "4.5";
                case PropertyType.Double:
                    return "4.5";
                case PropertyType.Byte:
                    return "7";
                case PropertyType.Int16:
                    return "7";
                case PropertyType.Int32:
                    return "7";
                case PropertyType.Int64:
                    return "7";
                case PropertyType.Object:
                    return "null";
                case PropertyType.SByte:
                    return "7";
                case PropertyType.Single:
                    return "7";
                case PropertyType.String:
                    return "text";
                case PropertyType.TimeSpan:
                    return "12:32:12";
                case PropertyType.UInt16:
                    return "7";
                case PropertyType.UInt32:
                    return "7";
                case PropertyType.UInt64:
                    return "7";
                case PropertyType.Xml:
                    return "<xml/>";
                case PropertyType.Guid:
                    return "\"" + System.Guid.NewGuid().ToString() + "\"";
                case PropertyType.Binary:
                    return "null";
                default:
                    return "null";
            }
        }

        #endregion
        #region GetValueAsString

        public static string GetValueAsString(object value)
        {
            if (value == null)
                return "null";
            if (value is bool)
            {
                if ((bool)value)
                    return "true";
                else
                    return "false";
            }
            if (value is string)
            {
                if (value.ToString().Length == 0)
                    return "string.Empty";
                else
                    return "\"" + value.ToString() + "\"";
            }
            if (value is System.DateTime)
                return ((System.DateTime)value).ToString("s", System.Globalization.CultureInfo.InvariantCulture);
            if (value is double)
                return ((double)value).ToString(System.Globalization.CultureInfo.InvariantCulture);
            if (value is decimal)
                return ((decimal)value).ToString(System.Globalization.CultureInfo.InvariantCulture);
            if (value is float)
                return ((float)value).ToString(System.Globalization.CultureInfo.InvariantCulture);
            return value.ToString();
        }

        #endregion

        private const string Vowals = "aeiouy";

        #region Entity Framework

        private const string ExternalTypeNameAttributeName = @"http://schemas.microsoft.com/ado/2006/04/codegeneration:ExternalTypeName";

        private readonly CodeGenerationTools _code;
        private readonly MetadataTools _ef;

        public TypeMapper(CodeGenerationTools code, MetadataTools ef)
        {
            ArgumentNotNull(code, "code");
            ArgumentNotNull(ef, "ef");

            _code = code;
            _ef = ef;
        }

        public string GetTypeName(TypeUsage typeUsage)
        {
            return typeUsage == null ? null : GetTypeName(typeUsage.EdmType, _ef.IsNullable(typeUsage), modelNamespace: null);
        }

        public string GetTypeName(EdmType edmType)
        {
            return GetTypeName(edmType, isNullable: null, modelNamespace: null);
        }

        public string GetTypeName(TypeUsage typeUsage, string modelNamespace)
        {
            return typeUsage == null ? null : GetTypeName(typeUsage.EdmType, _ef.IsNullable(typeUsage), modelNamespace);
        }

        public string GetTypeName(EdmType edmType, string modelNamespace)
        {
            return GetTypeName(edmType, isNullable: null, modelNamespace: modelNamespace);
        }

        public string GetTypeName(EdmType edmType, bool? isNullable, string modelNamespace)
        {
            if (edmType == null)
            {
                return null;
            }

            var collectionType = edmType as CollectionType;
            if (collectionType != null)
            {
                return String.Format(CultureInfo.InvariantCulture, "ICollection<{0}>", GetTypeName(collectionType.TypeUsage, modelNamespace));
            }

            var typeName = _code.Escape(edmType.MetadataProperties
                                    .Where(p => p.Name == ExternalTypeNameAttributeName)
                                    .Select(p => (string)p.Value)
                                    .FirstOrDefault())
                ?? (modelNamespace != null && edmType.NamespaceName != modelNamespace ?
                    _code.CreateFullName(_code.EscapeNamespace(edmType.NamespaceName), _code.Escape(edmType)) :
                    _code.Escape(edmType));

            if (edmType is StructuralType)
            {
                return typeName;
            }

            if (edmType is SimpleType)
            {
                var clrType = UnderlyingClrType(edmType);
                if (!IsEnumType(edmType))
                {
                    typeName = _code.Escape(clrType);
                }

                return clrType.IsValueType && isNullable == true ?
                    String.Format(CultureInfo.InvariantCulture, "System.Nullable<{0}>", typeName) :
                    typeName;
            }

            throw new ArgumentException("edmType");
        }

        public Type UnderlyingClrType(EdmType edmType)
        {
            ArgumentNotNull(edmType, "edmType");

            var primitiveType = edmType as PrimitiveType;
            if (primitiveType != null)
            {
                return primitiveType.ClrEquivalentType;
            }

            if (IsEnumType(edmType))
            {
                return GetEnumUnderlyingType(edmType).ClrEquivalentType;
            }

            return typeof(object);
        }

        public object GetEnumMemberValue(MetadataItem enumMember)
        {
            ArgumentNotNull(enumMember, "enumMember");

            var valueProperty = enumMember.GetType().GetProperty("Value");
            return valueProperty == null ? null : valueProperty.GetValue(enumMember, null);
        }

        public string GetEnumMemberName(MetadataItem enumMember)
        {
            ArgumentNotNull(enumMember, "enumMember");

            var nameProperty = enumMember.GetType().GetProperty("Name");
            return nameProperty == null ? null : (string)nameProperty.GetValue(enumMember, null);
        }

        public System.Collections.IEnumerable GetEnumMembers(EdmType enumType)
        {
            ArgumentNotNull(enumType, "enumType");

            var membersProperty = enumType.GetType().GetProperty("Members");
            return membersProperty != null
                ? (System.Collections.IEnumerable)membersProperty.GetValue(enumType, null)
                : Enumerable.Empty<MetadataItem>();
        }

        public bool EnumIsFlags(EdmType enumType)
        {
            ArgumentNotNull(enumType, "enumType");

            var isFlagsProperty = enumType.GetType().GetProperty("IsFlags");
            return isFlagsProperty != null && (bool)isFlagsProperty.GetValue(enumType, null);
        }

        public bool IsEnumType(GlobalItem edmType)
        {
            ArgumentNotNull(edmType, "edmType");

            return edmType.GetType().Name == "EnumType";
        }

        public PrimitiveType GetEnumUnderlyingType(EdmType enumType)
        {
            ArgumentNotNull(enumType, "enumType");

            return (PrimitiveType)enumType.GetType().GetProperty("UnderlyingType").GetValue(enumType, null);
        }

        public string CreateLiteral(object value)
        {
            if (value == null || value.GetType() != typeof(TimeSpan))
            {
                return _code.CreateLiteral(value);
            }

            return string.Format(CultureInfo.InvariantCulture, "new TimeSpan({0})", ((TimeSpan)value).Ticks);
        }

        public bool VerifyCaseInsensitiveTypeUniqueness(IEnumerable<string> types, string sourceFile)
        {
            ArgumentNotNull(types, "types");
            ArgumentNotNull(sourceFile, "sourceFile");

            var hash = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
            if (types.Any(item => !hash.Add(item)))
            {
                return false;
            }
            return true;
        }

        public IEnumerable<SimpleType> GetEnumItemsToGenerate(IEnumerable<GlobalItem> itemCollection)
        {
            return GetItemsToGenerate<SimpleType>(itemCollection)
                .Where(e => IsEnumType(e));
        }

        public IEnumerable<T> GetItemsToGenerate<T>(IEnumerable<GlobalItem> itemCollection) where T : EdmType
        {
            return itemCollection
                .OfType<T>()
                .Where(i => !i.MetadataProperties.Any(p => p.Name == ExternalTypeNameAttributeName))
                .OrderBy(i => i.Name);
        }

        public IEnumerable<string> GetAllGlobalItems(IEnumerable<GlobalItem> itemCollection)
        {
            return itemCollection
                .Where(i => i is EntityType || i is ComplexType || i is EntityContainer || IsEnumType(i))
                .Select(g => GetGlobalItemName(g));
        }

        public string GetGlobalItemName(GlobalItem item)
        {
            if (item is EdmType)
            {
                return ((EdmType)item).Name;
            }
            else
            {
                return ((EntityContainer)item).Name;
            }
        }

        public IEnumerable<EdmProperty> GetSimpleProperties(EntityType type)
        {
            return type.Properties.Where(p => p.TypeUsage.EdmType is SimpleType && p.DeclaringType == type);
        }

        public IEnumerable<EdmProperty> GetSimpleProperties(ComplexType type)
        {
            return type.Properties.Where(p => p.TypeUsage.EdmType is SimpleType && p.DeclaringType == type);
        }

        public IEnumerable<EdmProperty> GetComplexProperties(EntityType type)
        {
            return type.Properties.Where(p => p.TypeUsage.EdmType is ComplexType && p.DeclaringType == type);
        }

        public IEnumerable<EdmProperty> GetComplexProperties(ComplexType type)
        {
            return type.Properties.Where(p => p.TypeUsage.EdmType is ComplexType && p.DeclaringType == type);
        }

        public IEnumerable<EdmProperty> GetPropertiesWithDefaultValues(EntityType type)
        {
            return type.Properties.Where(p => p.TypeUsage.EdmType is SimpleType && p.DeclaringType == type && p.DefaultValue != null);
        }

        public IEnumerable<EdmProperty> GetPropertiesWithDefaultValues(ComplexType type)
        {
            return type.Properties.Where(p => p.TypeUsage.EdmType is SimpleType && p.DeclaringType == type && p.DefaultValue != null);
        }

        public IEnumerable<NavigationProperty> GetNavigationProperties(EntityType type)
        {
            return type.NavigationProperties.Where(np => np.DeclaringType == type);
        }

        public IEnumerable<NavigationProperty> GetCollectionNavigationProperties(EntityType type)
        {
            return type.NavigationProperties.Where(np => np.DeclaringType == type && np.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many);
        }

        public FunctionParameter GetReturnParameter(EdmFunction edmFunction)
        {
            ArgumentNotNull(edmFunction, "edmFunction");

            var returnParamsProperty = edmFunction.GetType().GetProperty("ReturnParameters");
            return returnParamsProperty == null
                ? edmFunction.ReturnParameter
                : ((IEnumerable<FunctionParameter>)returnParamsProperty.GetValue(edmFunction, null)).FirstOrDefault();
        }

        public bool IsComposable(EdmFunction edmFunction)
        {
            ArgumentNotNull(edmFunction, "edmFunction");

            var isComposableProperty = edmFunction.GetType().GetProperty("IsComposableAttribute");
            return isComposableProperty != null && (bool)isComposableProperty.GetValue(edmFunction, null);
        }

        public TypeUsage GetReturnType(EdmFunction edmFunction)
        {
            var returnParam = GetReturnParameter(edmFunction);
            return returnParam == null ? null : _ef.GetElementType(returnParam.TypeUsage);
        }

        public bool GenerateMergeOptionFunction(EdmFunction edmFunction, bool includeMergeOption)
        {
            var returnType = GetReturnType(edmFunction);
            return !includeMergeOption && returnType != null && returnType.EdmType.BuiltInTypeKind == BuiltInTypeKind.EntityType;
        }

        public static void ArgumentNotNull<T>(T arg, string name) where T : class
        {
            if (arg == null)
            {
                throw new ArgumentNullException(name);
            }
        }

        #endregion
    }
}
